<?php
// $Id: cron.inc 449 2017-10-22 06:33:45Z east $

/**
 * 计划任务统一管理脚本，请将以下任务加入到系统计划任务中，间隔时间自行控制，建议为 1 分钟，以免遗漏
 *  # Dida 计划任务调度 
 *  * * * * * php $path/scripts/cron.inc --host www.didaah.org
 *
 * 可使用本脚本手动执行任务，必须指定模块名称及任务机器名，手动执行时，不检查条件限制
 *  如：php $path/scripts/cron.inc --host www.didaah.org --cron_module system --cron_name test
 *  执行 system 模块的 test 任务
 *
 * @param 可用的参数
 *  --cron_module 指定模块名称，将只执行该模块设定的任务，注：旧名称为 --module，为避免和任务本身的参数冲突，添加 cron 前缀
 *  --cron_type 指定任务名称，将只执行该模块中指定名称的任务，注：旧名称为 --type，为避免和任务本身的参数冲突，添加 cron 前缀
 *
 *  在模块中定义任务的示例见 modules/system/cronjobs.inc
 */

$is_cron = true;

require_once 'common.inc';

if (!empty($_args['cron_module'])) {
  $__cron_module = $_args['cron_module'];
} else if (!empty($_args['module'])) {
  $__cron_module = $_args['module'];
}

if (!empty($_args['cron_name'])) {
  $__cron_name = $_args['cron_name'];
} else if (!empty($_args['name'])) {
  $__cron_name = $_args['name'];
}

if (!empty($__cron_module)) {
  module_include_file('cronjobs.inc', $__cron_module);
  $cronjobs = module_invoke($__cron_module, 'cronjobs', 0);

  if (!empty($__cron_name)) {
    // 默认设置一个开发调试任务
    if ($__cron_name == 'dida_dev_test' && empty($cronjobs[$__cron_name])) {
      $cronjobs['dida_dev_test'] = array(
        'name' => t('system', '开发调试'),
        'script' => 'test.inc',
        'param' => array('is_dida_dev_test' => 1),
        'year' => '2999',
        'month' => '13',
        'day' => '32', 
        'hour' => '15',
        'minute' => '20',
      );
    }
    $__crons = array($__cron_name);
  } else {
    $__crons = array_keys($cronjobs);
  }

  if (!empty($__crons)) {
    foreach ($__crons as $__name) {
      // 修改任务状态为正在执行
      if (!empty($_args['dida_cron_id'])) {
        db_exec('UPDATE {cron} SET status = 1, updated = ? WHERE cid = ?', array(time(), $_args['dida_cron_id']));
      }

      if (!empty($cronjobs[$__name])) {
        $__cron = $cronjobs[$__name];
        $module = !empty($__cron['module']) ? $__cron['module'] : $__cron_module;
        if (!empty($__cron['param'])) {
          if (is_array($__cron['param'])) {
            // 合并参数，以命令传递的参数为优先
            $_args = array_merge($__cron['param'], $_args);
            $__call_param = $_args;
          } else {
            $__call_param = $__cron['param'];
          }
        } else {
          $__call_param = NULL;
        }

        if (!empty($__cron['script'])) {
          // 注意，不要在脚本中使用 exit，否则回调函数及cron状态修改无法正常工作，返回请用 return
          // 注意，脚本中的变量命名，不要与此处冲突。脚本中尽量使用函数。
          $file = DIDA_ROOT . '/' . dd_get_path('module', $module) . '/scripts/' . $__cron['script'];
          if (is_file($file)) {
            require_once $file;
          } else {
            message(t('system', '文件 !file 不存在', array('!module' => $file)));
          }
        }

        // 回调函数
        if (!empty($__cron['call_func']) && function_exists($__cron['call_func'])) {
          if ($success_data = $__cron['call_func']($__call_param)) {
            if (!empty($__cron['success_func']) && function_exists($__cron['success_func'])) {
              $__cron['success_func']($success_data);
            }
          }
        }
      } else {
        message(t('system', '!module 模块没有任务 !name', array('!module' => $__cron_module, '!name' => $__cron_name)));
      }
      // 修改任务状态为执行完成
      // 注意：如果脚本中有 exit，将导致任务状态无法修改
      if (!empty($_args['dida_cron_id'])) {
        db_exec('UPDATE {cron} SET status = 2, updated = ? WHERE cid = ?', array(time(), $_args['dida_cron_id']));
      }
    }
  } else {
    message(t('system', '!module 模块没有任务', array('!module' => $__cron_module)));
  }

  module_invoke_all('exit', 'full');

  return;
}

// protected _cron_database() {{{ 
/**
 * 读取 cron 表中的任务，每次 10 条
 * 
 * @access protected
 * @return void
 */
function _cron_database() {
  // 每次可执行的最大任务数
  $limit = var_get('cron_limits', 100);
  
  $server_name = php_uname('n');

  $count = 0;

  // 获取所有 php 进程，找出正在执行的任务
  if ($process = _cron_get_php_ps()) {
    $count = count($process);
    if ($count >= var_get('cron_limits', 100)) {
      dd_log('cron_info', t('system', '已有 !count 个任务正在执行中，服务器：!system', array(
        '!count' => $count,
        '!system' => $server_name 
      )));
      // 只执行一个任务，防止优先级最高的 system system 任务未运行
      $limit = 1;
    } else {
      $limit -= $count;
    }
  }

  // 本次执行任务计数
  $cron_counts = 0;

  global $_args, $db;

  // 模块所有任务
  module_include_file('cronjobs.inc');
  $cronjobs = _module_invoke_all('cronjobs', 0);

  $max_date = date('ymdHi')+1;

  // 锁表操作,避免多台服务器同时读取任务时，造成冲突
  $db['default']->beginTransaction();

  if ($fetch = db_query('SELECT * FROM {cron} WHERE status = 0 AND date < ? 
  ORDER BY weight ASC, cid ASC LIMIT 0, ' . $limit . ' FOR UPDATE', array($max_date))) {
    foreach ($fetch as $cron) {
      $cron->data = !empty($cron->data) ? unserialize($cron->data) : array();

      $cron_counts++;

      // 任务后台执行命令，必须有 exec 函数权限，且 php 命令加入环境变量 
      $command = 'php ' . DIDA_ROOT . '/scripts/cron.inc --host ' . $_args['host'];
      $command .= ' --is_dida_cron 1 --dida_cron_id ' . $cron->cid;
      $command .= ' --hash' . $cron->cid . 'hash 1';

      if (!empty($cron->data['module']) && !empty($cron->data['type']) && !empty($cronjobs[$cron->data['module']]) 
      && !empty($cronjobs[$cron->data['module']][$cron->data['type']])) {
        // 在 hook_cronjobs 中定义的任务，调用其设置来完成后台执行
        $command .= ' --cron_name ' . $cron->data['type'] . ' --cron_module ' . $cron->data['module'];
      } else {
        // 未在 hook_cronjobs 中定义的任务，调用 system 模块 cron_default_script 任务来完成后台执行 
        $command .= ' --cron_name cron_default_script --cron_module system';
      }

      $cron->data['server'] = $server_name;
      $cron->data['command'] = $command;
      db_exec('UPDATE {cron} SET data = ?, updated = ?, status = ? WHERE cid = ?',
      array(serialize($cron->data), time(), 1, $cron->cid));

      // 后台执行
      exec($command . ' > /dev/null &');
    }
  }

  // 事务结束
  $db['default']->commit();

  // 如果库中已没有待执行的任务，则写入一个系统任务，该任务用于检查分析所有模块定义的任务是否需要执行
  if (empty($cron_counts)) {
    _cron_check_system();
  } else {
    dd_log('cron_info', t(
      'system',
      '服务器原有任务进程 !old 个，运行了 !count 个任务进程，耗时 !time 秒，服务器：!system',
      array(
        '!count' => $cron_counts,
        '!old' => $count,
        '!system' => $server_name, 
        '!time' => round(microtime(true)-DIDA_START_TIME, 4)
      )
    ));
  }
}
// }}}

// protected _cron_check_system() {{{ 
/**
 * _cron_check_system
 *  检查 system 模块当前时间是否有 system 任务，若不存在，则写入。
 * 
 * @access protected
 * @return void
 */
function _cron_check_system() {
  $date = date('ymdHi');
  $type = md5('systemsystem');

  if (!db_query('SELECT cid FROM {cron} WHERE type = ? AND date = ?',
  array($type, $date), array('return' => 'column', 'limit' => 1))) {
    $value = array(
      ':type' =>$type, 
      ':date' => $date,
      ':created' => time(),
      ':status' => 0,
      ':weight' => 100, 
      ':data' => serialize(array(
        '#id' => '999',
        'module' => 'system',
        'type' => 'system'
      ))
    );
    
    db_exec('INSERT INTO {cron} SET type = :type, date = :date, created = :created, 
    status = :status, weight = :weight, data = :data', $value);
  }
}
// }}}

_cron_database();

module_invoke_all('exit', 'full');
